Compare commits

...

8 Commits
v6.3 ... v6.3.1

Author SHA1 Message Date
cf33cb66c6 Fix release build 2023-11-23 13:25:12 +02:00
Макар Разин
8010c5079b Translated using Weblate (Ukrainian)
Currently translated at 100.0% (524 of 524 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (524 of 524 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (524 of 524 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-11-23 12:47:59 +02:00
a87a77083e Reader fixes 2023-11-23 12:42:43 +02:00
ca20422344 Support Paused manga state 2023-11-23 12:33:48 +02:00
c213b9d4b5 Update parsers 2023-11-23 12:26:38 +02:00
95fbe496cb Search through all sources in catalog 2023-11-22 16:11:23 +02:00
b9fd2e100d Fallback to local manga on network error 2023-11-22 15:38:39 +02:00
1242a88f8e Fix disabling webtoon scale 2023-11-22 15:10:26 +02:00
19 changed files with 131 additions and 56 deletions

View File

@@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdk = 21
targetSdk = 34
versionCode = 597
versionName = '6.3.0'
versionCode = 598
versionName = '6.3.1'
generatedDensities = []
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
ksp {
@@ -82,7 +82,7 @@ afterEvaluate {
}
dependencies {
//noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:41eea1c420') {
implementation('com.github.KotatsuApp:kotatsu-parsers:c3613f3ba4') {
exclude group: 'org.json', module: 'json'
}

View File

@@ -5,6 +5,7 @@ import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
@@ -19,19 +20,14 @@ class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSourc
override val configKeyDomain: ConfigKey.Domain
get() = ConfigKey.Domain("")
override val sortOrders: Set<SortOrder>
override val availableSortOrders: Set<SortOrder>
get() = EnumSet.allOf(SortOrder::class.java)
override suspend fun getDetails(manga: Manga): Manga {
TODO("Not yet implemented")
}
override suspend fun getList(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
TODO("Not yet implemented")
}
@@ -39,7 +35,7 @@ class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSourc
TODO("Not yet implemented")
}
override suspend fun getTags(): Set<MangaTag> {
override suspend fun getAvailableTags(): Set<MangaTag> {
TODO("Not yet implemented")
}
}

View File

@@ -11,8 +11,8 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.runInterruptible
import okio.IOException
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.parser.MangaRepository
@@ -33,7 +33,6 @@ class DetailsLoadUseCase @Inject constructor(
private val mangaRepositoryFactory: MangaRepository.Factory,
private val recoverUseCase: RecoverMangaUseCase,
private val imageGetter: Html.ImageGetter,
private val networkState: NetworkState,
) {
operator fun invoke(intent: MangaIntent): Flow<MangaDetails> = channelFlow {
@@ -48,16 +47,15 @@ class DetailsLoadUseCase @Inject constructor(
null
}
send(MangaDetails(manga, null, null, false))
if (!networkState.value) {
// try load offline instead
local?.await()?.manga?.let { localManga ->
try {
val details = getDetails(manga)
send(MangaDetails(details, local?.peek(), details.description?.parseAsHtml(withImages = false), false))
send(MangaDetails(details, local?.await(), details.description?.parseAsHtml(withImages = true), true))
} catch (e: IOException) {
local?.await()?.manga?.also { localManga ->
send(MangaDetails(localManga, null, localManga.description?.parseAsHtml(withImages = false), true))
return@channelFlow
}
} ?: throw e
}
val details = getDetails(manga)
send(MangaDetails(details, local?.peek(), details.description?.parseAsHtml(withImages = false), false))
send(MangaDetails(details, local?.await(), details.description?.parseAsHtml(withImages = true), true))
}
private suspend fun getDetails(seed: Manga) = runCatchingCancellable {

View File

@@ -197,6 +197,11 @@ class DetailsFragment :
drawableTop = ContextCompat.getDrawable(context, R.drawable.ic_state_abandoned)
}
MangaState.PAUSED -> infoLayout.textViewState.apply {
textAndVisible = resources.getString(R.string.state_paused)
drawableTop = ContextCompat.getDrawable(context, R.drawable.ic_action_pause)
}
null -> infoLayout.textViewState.isVisible = false
}
if (manga.source == MangaSource.LOCAL) {

View File

@@ -27,7 +27,6 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
@@ -119,9 +118,12 @@ class ReaderViewModel @Inject constructor(
valueProducer = { isReaderKeepScreenOn },
)
val isWebtoonZooEnabled = observeIsWebtoonZoomEnabled()
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
val isZoomControlsEnabled = getObserveIsZoomControlEnabled().flatMapLatest { zoom ->
if (zoom) {
combine(readerMode, observeIsWebtoonZoomEnabled()) { mode, ze -> ze || mode != ReaderMode.WEBTOON }
combine(readerMode, isWebtoonZooEnabled) { mode, ze -> ze || mode != ReaderMode.WEBTOON }
} else {
flowOf(false)
}
@@ -260,8 +262,8 @@ class ReaderViewModel @Inject constructor(
stateChangeJob = launchJob(Dispatchers.Default) {
prevJob?.cancelAndJoin()
loadingJob?.join()
if (BuildConfig.DEBUG && pages.size != content.value.pages.size) {
throw IllegalStateException("Concurrent pages modification")
if (pages.size != content.value.pages.size) {
return@launchJob // TODO
}
pages.getOrNull(position)?.let { page ->
currentState.update { cs ->

View File

@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.util.ext.findCenterViewPosition
import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.ReaderState
@@ -44,6 +45,9 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
adapter = readerAdapter
addOnPageScrollListener(PageScrollListener())
}
viewModel.isWebtoonZooEnabled.observe(viewLifecycleOwner) {
binding.frame.isZoomEnable = it
}
}
override fun onDestroyView() {

View File

@@ -51,7 +51,7 @@ class WebtoonScalingFrame @JvmOverloads constructor(
private val targetHitRect = Rect()
private var animator: ValueAnimator? = null
var isZoomEnable = true
var isZoomEnable = false
set(value) {
field = value
if (scale != 1f) {

View File

@@ -8,7 +8,8 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
sealed interface SourceCatalogItem : ListModel {
data class Source(
val source: MangaSource
val source: MangaSource,
val showSummary: Boolean,
) : SourceCatalogItem {
override fun areItemsTheSame(other: ListModel): Boolean {

View File

@@ -6,6 +6,7 @@ import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getSummary
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
@@ -43,6 +44,12 @@ fun sourceCatalogItemSourceAD(
} else {
item.source.title
}
if (item.showSummary) {
binding.textViewDescription.text = item.source.getSummary(context)
binding.textViewDescription.isVisible = true
} else {
binding.textViewDescription.isVisible = false
}
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
crossfade(context)

View File

@@ -1,9 +1,12 @@
package org.koitharu.kotatsu.settings.sources.catalog
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.widget.SearchView
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import coil.ImageLoader
import com.google.android.material.appbar.AppBarLayout
@@ -28,7 +31,7 @@ import javax.inject.Inject
class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
TabLayout.OnTabSelectedListener,
OnListItemClickListener<SourceCatalogItem.Source>,
AppBarOwner {
AppBarOwner, MenuItem.OnActionExpandListener {
@Inject
lateinit var coil: ImageLoader
@@ -56,7 +59,7 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
viewModel.locale.observe(this) {
supportActionBar?.subtitle = it.getLocaleDisplayName()
}
addMenuProvider(SourcesCatalogMenuProvider(this, viewModel))
addMenuProvider(SourcesCatalogMenuProvider(this, viewModel, this))
}
override fun onWindowInsetsChanged(insets: Insets) {
@@ -83,6 +86,19 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
viewBinding.recyclerView.firstVisibleItemPosition = 0
}
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
viewBinding.tabs.isVisible = false
val sq = (item.actionView as? SearchView)?.query?.trim()?.toString().orEmpty()
viewModel.performSearch(sq)
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
viewBinding.tabs.isVisible = true
viewModel.performSearch(null)
return true
}
private fun initTabs() {
val tabs = viewBinding.tabs
for (type in ContentType.entries) {

View File

@@ -28,7 +28,7 @@ class SourcesCatalogListProducer @AssistedInject constructor(
) : InvalidationTracker.Observer(TABLE_SOURCES), RetainedLifecycle.OnClearedListener {
private val scope = lifecycle.lifecycleScope
private var query: String = ""
private var query: String? = null
val list = MutableStateFlow(emptyList<SourceCatalogItem>())
private var job = scope.launch(Dispatchers.Default) {
@@ -54,20 +54,21 @@ class SourcesCatalogListProducer @AssistedInject constructor(
}
}
fun setQuery(value: String) {
fun setQuery(value: String?) {
this.query = value
onInvalidated(emptySet())
}
private suspend fun buildList(): List<SourceCatalogItem> {
val sources = repository.getDisabledSources().toMutableList()
sources.retainAll { it.contentType == contentType && it.locale == locale }
if (query.isNotEmpty()) {
sources.retainAll { it.title.contains(query, ignoreCase = true) }
when (val q = query) {
null -> sources.retainAll { it.contentType == contentType && it.locale == locale }
"" -> return emptyList()
else -> sources.retainAll { it.title.contains(q, ignoreCase = true) }
}
return if (sources.isEmpty()) {
listOf(
if (query.isEmpty()) {
if (query == null) {
SourceCatalogItem.Hint(
icon = R.drawable.ic_empty_feed,
title = R.string.no_manga_sources,
@@ -86,6 +87,7 @@ class SourcesCatalogListProducer @AssistedInject constructor(
sources.map {
SourceCatalogItem.Source(
source = it,
showSummary = query != null,
)
}
}

View File

@@ -15,6 +15,7 @@ import org.koitharu.kotatsu.parsers.util.toTitleCase
class SourcesCatalogMenuProvider(
private val activity: Activity,
private val viewModel: SourcesCatalogViewModel,
private val expandListener: MenuItem.OnActionExpandListener,
) : MenuProvider,
MenuItem.OnActionExpandListener,
SearchView.OnQueryTextListener {
@@ -40,18 +41,18 @@ class SourcesCatalogMenuProvider(
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
(activity as? AppBarOwner)?.appBar?.setExpanded(false, true)
return true
return expandListener.onMenuItemActionExpand(item)
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
(item.actionView as SearchView).setQuery("", false)
return true
return expandListener.onMenuItemActionCollapse(item)
}
override fun onQueryTextSubmit(query: String?): Boolean = false
override fun onQueryTextChange(newText: String?): Boolean {
viewModel.performSearch(newText.orEmpty())
viewModel.performSearch(newText?.trim().orEmpty())
return true
}

View File

@@ -29,11 +29,11 @@ import javax.inject.Inject
class SourcesCatalogViewModel @Inject constructor(
private val repository: MangaSourcesRepository,
private val listProducerFactory: SourcesCatalogListProducer.Factory,
private val settings: AppSettings,
settings: AppSettings,
) : BaseViewModel() {
private val lifecycle = RetainedLifecycleImpl()
private var searchQuery: String = ""
private var searchQuery: String? = null
val onActionDone = MutableEventFlow<ReversibleAction>()
val contentType = MutableStateFlow(ContentType.entries.first())
val locales = getLocalesImpl()
@@ -59,7 +59,7 @@ class SourcesCatalogViewModel @Inject constructor(
lifecycle.dispatchOnCleared()
}
fun performSearch(query: String) {
fun performSearch(query: String?) {
searchQuery = query
listProducer.value?.setQuery(query)
}

View File

@@ -23,17 +23,34 @@
app:shapeAppearance="?shapeAppearanceCornerSmall"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/textView_title"
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="?android:listPreferredItemPaddingStart"
android:layout_marginEnd="?android:listPreferredItemPaddingEnd"
android:layout_weight="1"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceTitleSmall"
tools:text="@tools:sample/lorem[15]" />
android:orientation="vertical">
<TextView
android:id="@+id/textView_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceTitleSmall"
tools:text="@tools:sample/lorem[15]" />
<TextView
android:id="@+id/textView_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodySmall"
tools:text="English" />
</LinearLayout>
<ImageView
android:id="@+id/imageView_add"

View File

@@ -506,4 +506,18 @@
<string name="backups_output_directory">Вывадны каталог рэзервовых копій</string>
<string name="speed_value">x%.1f</string>
<string name="lock_screen_rotation">Блакаванне павароту экрана</string>
<string name="sources_catalog">Каталог крыніц</string>
<string name="content_type_manga">Манга</string>
<string name="source_summary_pattern">%1$s, %2$s</string>
<string name="content_type_hentai">Хентай</string>
<string name="content_type_comics">Коміксы</string>
<string name="catalog">Каталог</string>
<string name="manage_sources">Кіраванне крыніцамі</string>
<string name="no_manga_sources_found">Па вашаму запыту не знойдзена даступных крыніц мангі</string>
<string name="manual">Уручную</string>
<string name="source_enabled">Крыніца ўключана</string>
<string name="disable_nsfw_summary">Адключыць крыніцы NSFW і схавайць мангу для дарослых са спісу, калі гэта магчыма</string>
<string name="no_manga_sources_catalog_text">У гэтым раздзеле пакуль няма даступных крыніц. Сачыце за абнаўленнямі</string>
<string name="available_d">Даступна: %1$d</string>
<string name="content_type_other">Іншае</string>
</resources>

View File

@@ -519,4 +519,5 @@
<string name="no_manga_sources_catalog_text">Пока что в данном разделе нет доступных источников. Следите за обновлениями</string>
<string name="available_d">Доступно: %1$d</string>
<string name="content_type_other">Другое</string>
<string name="source_summary_pattern">%1$s, %2$s</string>
</resources>

View File

@@ -506,4 +506,18 @@
<string name="backups_output_directory">Вихідний каталог резервних копій</string>
<string name="speed_value">x%.1f</string>
<string name="lock_screen_rotation">Блокування повороту екрану</string>
<string name="sources_catalog">Каталог джерел</string>
<string name="content_type_manga">Манґа</string>
<string name="source_summary_pattern">%1$s, %2$s</string>
<string name="content_type_hentai">Хентай</string>
<string name="content_type_comics">Комікси</string>
<string name="catalog">Каталог</string>
<string name="manage_sources">Кіравання джерелами</string>
<string name="no_manga_sources_found">За вашим запитом не знайдено доступних джерел манґи</string>
<string name="manual">Вручну</string>
<string name="source_enabled">Джерело включено</string>
<string name="disable_nsfw_summary">Вимкніть джерела NSFW і приховайте манґу для дорослих зі списку, якщо можливо</string>
<string name="no_manga_sources_catalog_text">У цьому розділі ще немає доступних джерел. Слідкуйте за оновленнями</string>
<string name="available_d">Доступно: %1$d</string>
<string name="content_type_other">Інший</string>
</resources>

View File

@@ -519,11 +519,12 @@
<string name="source_summary_pattern">%1$s, %2$s</string>
<string name="sources_catalog">Sources catalog</string>
<string name="source_enabled">Source enabled</string>
<string name="no_manga_sources_catalog_text">No available sources in this section yet. Stay tuned</string>
<string name="no_manga_sources_catalog_text">There are no sources available in this section, or all of it might have been already added.\nStay tuned</string>
<string name="no_manga_sources_found">No available manga sources found by your query</string>
<string name="catalog">Catalog</string>
<string name="manage_sources">Manage sources</string>
<string name="manual">Manual</string>
<string name="available_d">Available: %1$d</string>
<string name="disable_nsfw_summary">Disable NSFW sources and hide adult manga from list if possible</string>
<string name="state_paused">Paused</string>
</resources>

View File

@@ -6,6 +6,7 @@ import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
@@ -17,21 +18,16 @@ class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSourc
override val configKeyDomain: ConfigKey.Domain
get() = ConfigKey.Domain("localhost")
override val sortOrders: Set<SortOrder>
override val availableSortOrders: Set<SortOrder>
get() = EnumSet.allOf(SortOrder::class.java)
override suspend fun getDetails(manga: Manga): Manga = stub()
override suspend fun getList(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> = stub()
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> = stub()
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = stub()
override suspend fun getTags(): Set<MangaTag> = stub()
override suspend fun getAvailableTags(): Set<MangaTag> = stub()
private fun stub(): Nothing {
throw NotFoundException("Usage of Dummy parser in release build", "")