Merge branch 'master' into devel

This commit is contained in:
Koitharu
2021-06-26 15:19:50 +03:00
22 changed files with 87 additions and 162 deletions

65
.idea/misc.xml generated
View File

@@ -1,65 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DesignSurface">
<option name="filePathToZoomLevelMap">
<map>
<entry key="../../../../../../layout/custom_preview.xml" value="0.1" />
<entry key="../../../../../../opt/usr/android-sdk/platforms/android-30/data/res/drawable/list_divider_material.xml" value="0.28512820512820514" />
<entry key="../../../../../../opt/usr/android-sdk/platforms/android-30/data/res/layout/simple_dropdown_item_1line.xml" value="0.24739583333333334" />
<entry key="../../../../.gradle/caches/transforms-3/0998d1b3fbd6b77213a827054a7dfcfd/transformed/appcompat-1.2.0/res/layout/abc_alert_dialog_material.xml" value="0.25885416666666666" />
<entry key="../../../../.gradle/caches/transforms-3/0998d1b3fbd6b77213a827054a7dfcfd/transformed/appcompat-1.2.0/res/layout/abc_select_dialog_material.xml" value="0.25885416666666666" />
<entry key="../../../../.gradle/caches/transforms-3/688e95ad986d2d0286c79f787589b7cb/transformed/material-1.3.0/res/layout/mtrl_alert_dialog.xml" value="0.25885416666666666" />
<entry key="../../../../.gradle/caches/transforms-3/7bbda65156c2f797f689a169a6aaa2eb/transformed/appcompat-1.2.0/res/drawable/abc_switch_thumb_material.xml" value="0.2609375" />
<entry key="app/src/main/res/drawable/ic_alert_outline.xml" value="0.2609375" />
<entry key="app/src/main/res/drawable/ic_clear_all.xml" value="0.275" />
<entry key="app/src/main/res/drawable/ic_complete.xml" value="0.275" />
<entry key="app/src/main/res/drawable/ic_history.xml" value="0.275" />
<entry key="app/src/main/res/drawable/ic_locale.xml" value="0.197" />
<entry key="app/src/main/res/drawable/ic_open_external.xml" value="0.197" />
<entry key="app/src/main/res/drawable/tab_indicator.xml" value="0.28512820512820514" />
<entry key="app/src/main/res/drawable/tabs_background.xml" value="0.28512820512820514" />
<entry key="app/src/main/res/layout-w600dp/activity_details.xml" value="0.18072916666666666" />
<entry key="app/src/main/res/layout-w600dp/fragment_details.xml" value="0.14583333333333334" />
<entry key="app/src/main/res/layout-w600dp/fragment_list.xml" value="0.14635416666666667" />
<entry key="app/src/main/res/layout/activity_protect.xml" value="0.26927083333333335" />
<entry key="app/src/main/res/layout/activity_setup_protect.xml" value="0.26927083333333335" />
<entry key="app/src/main/res/layout/dialog_favorite_categories.xml" value="0.2601851851851852" />
<entry key="app/src/main/res/layout/dialog_list_mode.xml" value="0.2601851851851852" />
<entry key="app/src/main/res/layout/dialog_onboard.xml" value="0.25885416666666666" />
<entry key="app/src/main/res/layout/fragment_chapters.xml" value="0.24739583333333334" />
<entry key="app/src/main/res/layout/fragment_details.xml" value="0.26145833333333335" />
<entry key="app/src/main/res/layout/fragment_favourites.xml" value="0.26296296296296295" />
<entry key="app/src/main/res/layout/fragment_feed.xml" value="0.2601851851851852" />
<entry key="app/src/main/res/layout/fragment_list.xml" value="0.2601851851851852" />
<entry key="app/src/main/res/layout/fragment_search_suggestion.xml" value="0.25885416666666666" />
<entry key="app/src/main/res/layout/item_branch.xml" value="0.24739583333333334" />
<entry key="app/src/main/res/layout/item_branch_dropdown.xml" value="0.25743589743589745" />
<entry key="app/src/main/res/layout/item_category.xml" value="0.25885416666666666" />
<entry key="app/src/main/res/layout/item_category_checkable.xml" value="0.2601851851851852" />
<entry key="app/src/main/res/layout/item_manga_grid.xml" value="0.26042632066728455" />
<entry key="app/src/main/res/layout/item_manga_list_details.xml" value="0.2601851851851852" />
<entry key="app/src/main/res/layout/item_page_thumb.xml" value="0.2601851851851852" />
<entry key="app/src/main/res/layout/item_page_webtoon.xml" value="0.13095238095238096" />
<entry key="app/src/main/res/layout/item_recent.xml" value="0.2601851851851852" />
<entry key="app/src/main/res/layout/item_search_suggestion_header.xml" value="0.25885416666666666" />
<entry key="app/src/main/res/layout/item_search_suggestion_manga.xml" value="0.24479166666666666" />
<entry key="app/src/main/res/layout/item_search_suggestion_query.xml" value="0.587248322147651" />
<entry key="app/src/main/res/layout/item_source_config.xml" value="0.25885416666666666" />
<entry key="app/src/main/res/layout/item_source_locale.xml" value="0.25885416666666666" />
<entry key="app/src/main/res/layout/item_tracklog.xml" value="0.24479166666666666" />
<entry key="app/src/main/res/layout/sheet_pages.xml" value="0.2601851851851852" />
<entry key="app/src/main/res/menu/opt_browser.xml" value="0.24479166666666666" />
<entry key="app/src/main/res/menu/opt_protect.xml" value="0.26927083333333335" />
<entry key="app/src/main/res/menu/opt_sources.xml" value="0.24479166666666666" />
<entry key="app/src/main/res/menu/popup_category.xml" value="0.2601851851851852" />
<entry key="app/src/main/res/xml/pref_main.xml" value="0.26927083333333335" />
</map>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

View File

@@ -13,8 +13,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 30
versionCode 364
versionName '1.0.1'
versionCode 365
versionName '1.1'
kapt {
arguments {
@@ -93,9 +93,8 @@ dependencies {
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.0'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.0'
implementation 'io.insert-koin:koin-android:3.0.2'
implementation 'io.insert-koin:koin-android-ext:3.0.2'
implementation 'io.coil-kt:coil-base:1.2.1'
implementation 'io.insert-koin:koin-android:3.1.0'
implementation 'io.coil-kt:coil-base:1.2.2'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'com.github.solkin:disk-lru-cache:1.2'
@@ -103,5 +102,5 @@ dependencies {
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.json:json:20210307'
testImplementation 'io.insert-koin:koin-test-junit4:3.0.2'
testImplementation 'io.insert-koin:koin-test-junit4:3.1.0'
}

View File

@@ -35,6 +35,7 @@ class ShortcutsRepository(
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) return
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
val shortcuts = historyRepository.getList(0, manager.maxShortcutCountPerActivity)
.filter { x -> x.title.isNotEmpty() }
.map { buildShortcutInfo(it).build().toShortcutInfo() }
manager.dynamicShortcuts = shortcuts
}

View File

@@ -49,18 +49,17 @@ class MangareadRepository(
id = generateUid(href),
url = href,
publicUrl = href.inContextOf(div),
coverUrl = div.selectFirst("img").attr("data-srcset")
.split(',').firstOrNull()?.substringBeforeLast(' ').orEmpty(),
coverUrl = div.selectFirst("img").absUrl("src"),
title = summary.selectFirst("h3").text(),
rating = div.selectFirst("span.total_votes")?.ownText()
?.toFloatOrNull()?.div(5f) ?: -1f,
tags = summary.selectFirst(".mg_genres").select("a").mapToSet { a ->
tags = summary.selectFirst(".mg_genres")?.select("a")?.mapToSet { a ->
MangaTag(
key = a.attr("href").removeSuffix("/").substringAfterLast('/'),
title = a.text(),
source = MangaSource.MANGAREAD
)
},
}.orEmpty(),
author = summary.selectFirst(".mg_author")?.selectFirst("a")?.ownText(),
state = when (summary.selectFirst(".mg_status")?.selectFirst(".summary-content")
?.ownText()?.trim()) {
@@ -148,7 +147,7 @@ class MangareadRepository(
?: throw ParseException("Root not found")
return root.select("div.page-break").map { div ->
val img = div.selectFirst("img")
val url = img.relUrl("data-src")
val url = img.relUrl("src")
MangaPage(
id = generateUid(url),
url = url,

View File

@@ -2,13 +2,12 @@ package org.koitharu.kotatsu.details
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.details.ui.DetailsViewModel
val detailsModule
get() = module {
viewModel { (intent: MangaIntent) ->
DetailsViewModel(intent, get(), get(), get(), get(), get(), get())
viewModel { intent ->
DetailsViewModel(intent.get(), get(), get(), get(), get(), get(), get())
}
}

View File

@@ -2,7 +2,6 @@ package org.koitharu.kotatsu.favourites
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel
import org.koitharu.kotatsu.favourites.ui.categories.select.MangaCategoriesViewModel
@@ -13,11 +12,11 @@ val favouritesModule
single { FavouritesRepository(get()) }
viewModel { (categoryId: Long) ->
FavouritesListViewModel(categoryId, get(), get())
viewModel { categoryId ->
FavouritesListViewModel(categoryId.get(), get(), get())
}
viewModel { FavouritesCategoriesViewModel(get()) }
viewModel { (manga: Manga) ->
MangaCategoriesViewModel(manga, get())
viewModel { manga ->
MangaCategoriesViewModel(manga.get(), get())
}
}

View File

@@ -4,7 +4,7 @@ import android.content.Context
import com.tomclaw.cache.DiskLruCache
import org.koitharu.kotatsu.utils.FileSizeUtils
import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.sub
import org.koitharu.kotatsu.utils.ext.subdir
import org.koitharu.kotatsu.utils.ext.takeIfReadable
import java.io.File
import java.io.InputStream
@@ -13,8 +13,10 @@ import java.io.OutputStream
class PagesCache(context: Context) {
private val cacheDir = context.externalCacheDir ?: context.cacheDir
private val lruCache =
DiskLruCache.create(cacheDir.sub(Cache.PAGES.dir), FileSizeUtils.mbToBytes(200))
private val lruCache = DiskLruCache.create(
cacheDir.subdir(Cache.PAGES.dir),
FileSizeUtils.mbToBytes(200)
)
operator fun get(url: String): File? {
return lruCache.get(url)?.takeIfReadable()
@@ -22,7 +24,7 @@ class PagesCache(context: Context) {
@Deprecated("Useless lambda")
fun put(url: String, writer: (OutputStream) -> Unit): File {
val file = cacheDir.sub(url.longHashCode().toString())
val file = File(cacheDir, url.longHashCode().toString())
file.outputStream().use(writer)
val res = lruCache.put(url, file)
file.delete()
@@ -30,7 +32,7 @@ class PagesCache(context: Context) {
}
fun put(url: String, inputStream: InputStream): File {
val file = cacheDir.sub(url.longHashCode().toString())
val file = File(cacheDir, url.longHashCode().toString())
file.outputStream().use { out ->
inputStream.copyTo(out)
}

View File

@@ -175,10 +175,12 @@ class LocalMangaRepository(private val context: Context) : MangaRepository {
}
fun getAvailableStorageDirs(context: Context): List<File> {
val result = ArrayList<File>(5)
result += context.filesDir.sub(DIR_NAME)
val result = ArrayList<File?>(5)
result += File(context.filesDir, DIR_NAME)
result += context.getExternalFilesDirs(DIR_NAME)
return result.distinctBy { it.canonicalPath }.filter { it.exists() || it.mkdir() }
return result.filterNotNull()
.distinctBy { it.canonicalPath }
.filter { it.exists() || it.mkdir() }
}
fun getFallbackStorageDir(context: Context): File? {

View File

@@ -3,9 +3,7 @@ package org.koitharu.kotatsu.reader
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
val readerModule
@@ -14,7 +12,7 @@ val readerModule
single { MangaDataRepository(get()) }
single { PagesCache(get()) }
viewModel { (intent: MangaIntent, state: ReaderState?) ->
ReaderViewModel(intent, state, get(), get(), get(), get())
viewModel { params ->
ReaderViewModel(params[0], params[1], get(), get(), get(), get())
}
}

View File

@@ -9,7 +9,7 @@ import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
val remoteListModule
get() = module {
viewModel { (source: MangaSource) ->
RemoteListViewModel(get(named(source)), get())
viewModel { source ->
RemoteListViewModel(get(named(source.get<MangaSource>())), get())
}
}

View File

@@ -1,15 +1,10 @@
package org.koitharu.kotatsu.remotelist.ui
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaFilter
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.search.ui.SearchActivity
import org.koitharu.kotatsu.utils.ext.parcelableArgument
import org.koitharu.kotatsu.utils.ext.withArgs
@@ -25,7 +20,7 @@ class RemoteListFragment : MangaListFragment() {
viewModel.loadNextPage()
}
override fun getTitle(): CharSequence? {
override fun getTitle(): CharSequence {
return source.title
}
@@ -34,19 +29,6 @@ class RemoteListFragment : MangaListFragment() {
super.onFilterChanged(filter)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.opt_remote, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.action_search_internal -> {
context?.startActivity(SearchActivity.newIntent(requireContext(), source, null))
true
}
else -> super.onOptionsItemSelected(item)
}
companion object {
private const val ARG_SOURCE = "provider"

View File

@@ -18,11 +18,11 @@ val searchModule
factory { MangaSuggestionsProvider.createSuggestions(androidContext()) }
viewModel { (source: MangaSource, query: String) ->
SearchViewModel(get(named(source)), query, get())
viewModel { params ->
SearchViewModel(get(named(params.get<MangaSource>(0))), params[1], get())
}
viewModel { (query: String) ->
GlobalSearchViewModel(query, get(), get())
viewModel { query ->
GlobalSearchViewModel(query.get(), get(), get())
}
viewModel { SearchSuggestionViewModel(get()) }
}

View File

@@ -96,7 +96,7 @@ class MangaSearchRepository(
MangaSuggestionsProvider.QUERY_URI,
SUGGESTION_PROJECTION,
null,
null,
arrayOfNulls(1),
null
)?.use { cursor -> cursor.count } ?: 0
}

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.settings
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
@@ -87,16 +88,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
true
}
AppSettings.KEY_SEARCH_HISTORY_CLEAR -> {
viewLifecycleScope.launch {
searchRepository.clearSearchHistory()
preference.summary = preference.context.resources
.getQuantityString(R.plurals.items, 0, 0)
Snackbar.make(
view ?: return@launch,
R.string.search_history_cleared,
Snackbar.LENGTH_SHORT
).show()
}
clearSearchHistory(preference)
true
}
AppSettings.KEY_UPDATES_FEED_CLEAR -> {
@@ -133,4 +125,23 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
}
}
}
private fun clearSearchHistory(preference: Preference) {
AlertDialog.Builder(context ?: return)
.setTitle(R.string.clear_search_history)
.setMessage(R.string.text_clear_search_history_prompt)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.clear) { _, _ ->
viewLifecycleScope.launch {
searchRepository.clearSearchHistory()
preference.summary = preference.context.resources
.getQuantityString(R.plurals.items, 0, 0)
Snackbar.make(
view ?: return@launch,
R.string.search_history_cleared,
Snackbar.LENGTH_SHORT
).show()
}
}.show()
}
}

View File

@@ -20,7 +20,9 @@ val settingsModule
single { AppSettings(androidContext()) }
viewModel { BackupViewModel(get(), androidContext()) }
viewModel { (uri: Uri?) -> RestoreViewModel(uri, get(), androidContext()) }
viewModel { params ->
RestoreViewModel(params.getOrNull(Uri::class), get(), androidContext())
}
viewModel { ProtectSetupViewModel(get()) }
viewModel { OnboardViewModel(get()) }
}

View File

@@ -8,7 +8,6 @@ import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
import org.koitharu.kotatsu.utils.ext.map
import org.koitharu.kotatsu.utils.ext.mapTo
import org.koitharu.kotatsu.utils.ext.mapToSet
import java.util.*
@@ -30,10 +29,10 @@ class OnboardViewModel(
if (settings.isSourcesSelected) {
selectedLocales.removeAll(settings.hiddenSources.map { x -> MangaSource.valueOf(x).locale })
} else {
LocaleListCompat.getDefault().mapTo(selectedLocales) { x ->
val deviceLocales = LocaleListCompat.getDefault().map { x ->
x.language
}
selectedLocales.retainAll(allSources.map { x -> x.locale })
selectedLocales.retainAll(deviceLocales)
if (selectedLocales.isEmpty()) {
selectedLocales += "en"
}
@@ -71,7 +70,7 @@ class OnboardViewModel(
}.sortedWith(SourceLocaleComparator())
}
private class SourceLocaleComparator : Comparator<SourceLocale> {
private class SourceLocaleComparator : Comparator<SourceLocale?> {
private val deviceLocales = LocaleListCompat.getAdjustedDefault()
.map { it.language }

View File

@@ -23,12 +23,12 @@ object CacheUtils {
@WorkerThread
fun computeCacheSize(context: Context, name: String) = getCacheDirs(context)
.map { it.sub(name) }
.map { File(it, name) }
.sumOf { x -> x.computeSize() }
@WorkerThread
fun clearCache(context: Context, name: String) = getCacheDirs(context)
.map { it.sub(name) }
.map { File(it, name) }
.forEach { it.deleteRecursively() }
// FIXME need async implementation

View File

@@ -13,8 +13,13 @@ import java.util.zip.ZipEntry
import java.util.zip.ZipFile
@Suppress("NOTHING_TO_INLINE")
@Deprecated("Useless", ReplaceWith("File(this, name)", "java.io.File"))
inline fun File.sub(name: String) = File(this, name)
fun File.subdir(name: String) = File(this, name).also {
if (!it.exists()) it.mkdirs()
}
fun File.takeIfReadable() = takeIf { it.exists() && it.canRead() }
fun ZipFile.readText(entry: ZipEntry) = getInputStream(entry).bufferedReader().use {

View File

@@ -80,7 +80,7 @@ fun String.toRelativeUrl(domain: String): String {
}
fun Element.relUrl(attributeKey: String): String {
val attr = attr(attributeKey)
val attr = attr(attributeKey).trim()
if (attr.isEmpty()) {
return ""
}

View File

@@ -50,18 +50,22 @@ fun String.toCamelCase(): String {
fun String.transliterate(skipMissing: Boolean): String {
val cyr = charArrayOf(
'a', 'б', 'в', 'г', 'д', 'ё', 'ж', 'з', 'и', 'к', 'л', 'м', 'н',
'п', 'р', 'с', 'т', 'у', 'ў', 'ф', 'х', 'ц', 'ш', 'щ', 'ы', 'э', 'ю', 'я'
'а', 'б', 'в', 'г', 'д', 'е', 'ж', 'з', 'и', 'й', 'к', 'л', 'м', 'н', 'о', 'п',
'р', 'с', 'т', 'у', 'ф', 'х', ', 'ч', 'ш', 'щ', 'ъ', 'ы', 'ь', 'э', 'ю', 'я', 'ё', 'ў'
)
val lat = arrayOf(
"a", "b", "v", "g", "d", "jo", "zh", "z", "i", "k", "l", "m", "n",
"p", "r", "s", "t", "u", "w", "f", "h", "ts", "sh", "sch", "", "e", "ju", "ja"
"a", "b", "v", "g", "d", "e", "zh", "z", "i", "y", "k", "l", "m", "n", "o", "p",
"r", "s", "t", "u", "f", "h", "ts", "ch", "sh", "sch", "", "i", "", "e", "ju", "ja", "jo", "w"
)
return buildString(length + 5) {
for (c in this@transliterate) {
val p = cyr.binarySearch(c.toLowerCase())
val p = cyr.binarySearch(c.lowercaseChar())
if (p in lat.indices) {
append(lat[p])
if (c.isUpperCase()) {
append(lat[p].uppercase())
} else {
append(lat[p])
}
} else if (!skipMissing) {
append(c)
}

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search_internal"
android:orderInCategory="26"
android:title="@string/search"
app:showAsAction="never" />
</menu>

View File

@@ -201,13 +201,13 @@
<string name="_and_x_more">…и ещё %1$d</string>
<string name="next">Далее</string>
<string name="protect_application_subtitle">Введите пароль, который вам понадобится при запуске приложения</string>
<string name="confirm">Confirm</string>
<string name="confirm">Подтвердить</string>
<string name="password_length_hint">Пароль должен содержать не менее 4 символов</string>
<string name="hide_toolbar">Прятать заголовок при прокрутке</string>
<string name="search_only_on_s">Поиск только по %s</string>
<string name="other">Другие</string>
<string name="languages">Languages</string>
<string name="welcome">Welcome</string>
<string name="description">Описание</string>
<string name="text_clear_search_history_prompt">Вы действительно хотите удалить все последние поисковые запросы? Это действие не может быть отменено.</string>
<string name="languages">Языки</string>
<string name="welcome">Добро пожаловать</string>
<string name="text_clear_search_history_prompt">Вы действительно хотите удалить все недавние поисковые запросы? Это действие не может быть отменено.</string>
</resources>