Support for multiple manga branches (translations, etc)

This commit is contained in:
Koitharu
2021-03-08 10:30:00 +02:00
parent 40f27ae634
commit 71f5ee8cb1
12 changed files with 154 additions and 17 deletions

View File

@@ -103,7 +103,7 @@ dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.6'
testImplementation 'junit:junit:4.13.1'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.json:json:20201115'
testImplementation 'org.koin:koin-test:2.2.2'
}

View File

@@ -9,5 +9,6 @@ data class MangaChapter(
val name: String,
val number: Int,
val url: String,
val branch: String? = null,
val source: MangaSource
) : Parcelable

View File

@@ -90,6 +90,8 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
chapters = ArrayList(total)
for (i in 0 until total) {
val item = list.getJSONObject(i)
val chapterId = item.getLong("chapter_id")
val branchName = item.getStringOrNull("username")
val url = buildString {
append(manga.url)
append("/v")
@@ -106,9 +108,10 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
}
chapters.add(
MangaChapter(
id = generateUid(url),
id = generateUid(chapterId),
url = url,
source = source,
branch = branchName,
number = total - i,
name = name
)

View File

@@ -3,13 +3,10 @@ package org.koitharu.kotatsu.details.ui
import android.app.ActivityOptions
import android.os.Bundle
import android.view.*
import android.widget.AdapterView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
@@ -17,6 +14,7 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.databinding.FragmentChaptersBinding
import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
@@ -25,7 +23,7 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState
class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
OnListItemClickListener<MangaChapter>, ActionMode.Callback {
OnListItemClickListener<MangaChapter>, ActionMode.Callback, AdapterView.OnItemSelectedListener {
private val viewModel by sharedViewModel<DetailsViewModel>()
@@ -53,9 +51,21 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
setHasFixedSize(true)
adapter = chaptersAdapter
}
val branchesAdapter = BranchesAdapter()
binding.spinnerBranches.adapter = branchesAdapter
binding.spinnerBranches.onItemSelectedListener = this
viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged)
viewModel.chapters.observe(viewLifecycleOwner, this::onChaptersChanged)
viewModel.branches.observe(viewLifecycleOwner) {
branchesAdapter.setItems(it)
binding.spinnerBranches.isVisible = it.size > 1
}
viewModel.selectedBranchIndex.observe(viewLifecycleOwner) {
if (it != -1 && it != binding.spinnerBranches.selectedItemPosition) {
binding.spinnerBranches.setSelection(it)
}
}
viewModel.isChaptersReversed.observe(viewLifecycleOwner) {
activity?.invalidateOptionsMenu()
}
@@ -64,6 +74,7 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
override fun onDestroyView() {
chaptersAdapter = null
selectionDecoration = null
binding.spinnerBranches.adapter = null
super.onDestroyView()
}
@@ -145,6 +156,12 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
}
}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
viewModel.setSelectedBranch(binding.spinnerBranches.selectedItem as String?)
}
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
val manga = viewModel.manga.value
mode.menuInflater.inflate(R.menu.mode_chapters, menu)
@@ -174,7 +191,7 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
binding.recyclerViewChapters.updatePadding(
left = insets.left,
right = insets.right,
bottom = insets.bottom
bottom = insets.bottom + binding.spinnerBranches.height
)
}

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.details.ui
import androidx.lifecycle.asFlow
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
@@ -18,6 +19,7 @@ import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.mapToSet
import java.io.IOException
class DetailsViewModel(
@@ -31,6 +33,7 @@ class DetailsViewModel(
) : BaseViewModel() {
private val mangaData = MutableStateFlow<Manga?>(intent.manga)
private val selectedBranch = MutableStateFlow<String?>(null)
private val history = mangaData.mapNotNull { it?.id }
.distinctUntilChanged()
@@ -69,12 +72,24 @@ class DetailsViewModel(
val onMangaRemoved = SingleLiveEvent<Manga>()
val branches = mangaData.map {
it?.chapters?.mapToSet { x -> x.branch }?.sortedBy { x -> x }.orEmpty()
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
val selectedBranchIndex = combine(
branches.asFlow(),
selectedBranch
) { branches, selected ->
branches.indexOf(selected)
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
val chapters = combine(
mangaData.map { it?.chapters.orEmpty() },
history.map { it?.chapterId },
newChapters,
chaptersReversed
) { chapters, currentId, newCount, reversed ->
chaptersReversed,
selectedBranch
) { chapters, currentId, newCount, reversed, branch ->
val currentIndex = chapters.indexOfFirst { it.id == currentId }
val firstNewIndex = chapters.size - newCount
val res = chapters.mapIndexed { index, chapter ->
@@ -86,7 +101,7 @@ class DetailsViewModel(
else -> ChapterExtra.UNREAD
}
)
}
}.filter { it.chapter.branch == branch }
if (reversed) res.asReversed() else res
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
@@ -96,6 +111,15 @@ class DetailsViewModel(
?: throw MangaNotFoundException("Cannot find manga")
mangaData.value = manga
manga = manga.source.repository.getDetails(manga)
// find default branch
val hist = historyRepository.getOne(manga)
selectedBranch.value = if (hist != null) {
manga.chapters?.find { it.id == hist.chapterId }?.branch
} else {
manga.chapters
?.groupBy { it.branch }
?.maxByOrNull { it.value.size }?.key
}
mangaData.value = manga
}
}
@@ -114,4 +138,8 @@ class DetailsViewModel(
fun setChaptersReversed(newValue: Boolean) {
settings.chaptersReverse = newValue
}
fun setSelectedBranch(branch: String?) {
selectedBranch.value = branch
}
}

View File

@@ -0,0 +1,45 @@
package org.koitharu.kotatsu.details.ui.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.TextView
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.ext.replaceWith
class BranchesAdapter : BaseAdapter() {
private val dataSet = ArrayList<String?>()
override fun getCount(): Int {
return dataSet.size
}
override fun getItem(position: Int): Any? {
return dataSet[position]
}
override fun getItemId(position: Int): Long {
return dataSet[position].hashCode().toLong()
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(parent.context)
.inflate(R.layout.item_branch, parent, false)
(view as TextView).text = dataSet[position]
return view
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(parent.context)
.inflate(R.layout.item_branch_dropdown, parent, false)
(view as TextView).text = dataSet[position]
return view
}
fun setItems(items: Collection<String?>) {
dataSet.replaceWith(items)
notifyDataSetChanged()
}
}

View File

@@ -40,7 +40,7 @@ class ReaderViewModel(
private var loadingJob: Job? = null
private val currentState = MutableStateFlow<ReaderState?>(null)
private val mangaData = MutableStateFlow<Manga?>(intent.manga)
private val mangaData = MutableStateFlow(intent.manga)
private val chapters = LongSparseArray<MangaChapter>()
val readerMode = MutableLiveData<ReaderMode>()
@@ -58,7 +58,7 @@ class ReaderViewModel(
)
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
val content = MutableLiveData<ReaderContent>(ReaderContent(emptyList(), null))
val content = MutableLiveData(ReaderContent(emptyList(), null))
val manga: Manga?
get() = mangaData.value
@@ -80,7 +80,6 @@ class ReaderViewModel(
manga.chapters?.forEach {
chapters.put(it.id, it)
}
mangaData.value = manga
// determine mode
val mode =
dataRepository.getReaderMode(manga.id) ?: manga.chapters?.randomOrNull()?.let {
@@ -96,6 +95,9 @@ class ReaderViewModel(
currentState.value = state ?: historyRepository.getOne(manga)?.let {
ReaderState.from(it)
} ?: ReaderState.initial(manga)
val branch = chapters[currentState.value?.chapterId ?: 0L].branch
mangaData.value = manga.copy(chapters = manga.chapters?.filter { it.branch == branch })
readerMode.postValue(mode)
val pages = loadChapter(requireNotNull(currentState.value).chapterId)

View File

@@ -1,11 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
style="@style/Widget.MaterialComponents.AppBarLayout.Surface"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$Behavior">
<Spinner
android:id="@+id/spinner_branches"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_scrollFlags="scroll|enterAlways"
tools:listitem="@layout/item_branch"
tools:visibility="visible" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView_chapters"
android:layout_width="match_parent"
@@ -18,6 +36,7 @@
app:fastScrollVerticalThumbDrawable="@drawable/list_thumb"
app:fastScrollVerticalTrackDrawable="@drawable/list_track"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:listitem="@layout/item_chapter" />
<ProgressBar
@@ -29,4 +48,4 @@
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<CheckedTextView
xmlns:android="http://schemas.android.com/apk/res/android"
style="?android:attr/spinnerItemStyle"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceLargePopupMenu" />

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<CheckedTextView
xmlns:android="http://schemas.android.com/apk/res/android"
style="?android:attr/spinnerDropDownItemStyle"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeightSmall"
android:drawableEnd="?android:listChoiceIndicatorSingle"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceLargePopupMenu" />