Support for multiple manga branches (translations, etc)
This commit is contained in:
@@ -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'
|
||||
}
|
||||
@@ -9,5 +9,6 @@ data class MangaChapter(
|
||||
val name: String,
|
||||
val number: Int,
|
||||
val url: String,
|
||||
val branch: String? = null,
|
||||
val source: MangaSource
|
||||
) : Parcelable
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
9
app/src/main/res/layout/item_branch.xml
Normal file
9
app/src/main/res/layout/item_branch.xml
Normal 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" />
|
||||
9
app/src/main/res/layout/item_branch_dropdown.xml
Normal file
9
app/src/main/res/layout/item_branch_dropdown.xml
Normal 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" />
|
||||
Reference in New Issue
Block a user