Support for multiple manga branches (translations, etc)

pull/26/head
Koitharu 5 years ago
parent 40f27ae634
commit 71f5ee8cb1

@ -4,8 +4,12 @@
<option name="filePathToZoomLevelMap"> <option name="filePathToZoomLevelMap">
<map> <map>
<entry key="../../../../../../layout/custom_preview.xml" value="0.1" /> <entry key="../../../../../../layout/custom_preview.xml" value="0.1" />
<entry key="../../../../../../opt/usr/android-sdk/platforms/android-30/data/res/layout/simple_dropdown_item_1line.xml" value="0.24739583333333334" />
<entry key="app/src/main/res/layout-w600dp/fragment_details.xml" value="0.14583333333333334" /> <entry key="app/src/main/res/layout-w600dp/fragment_details.xml" value="0.14583333333333334" />
<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_details.xml" value="0.26145833333333335" />
<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" />
</map> </map>
</option> </option>
</component> </component>

@ -103,7 +103,7 @@ dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.6' 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.json:json:20201115'
testImplementation 'org.koin:koin-test:2.2.2' testImplementation 'org.koin:koin-test:2.2.2'
} }

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

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

@ -3,13 +3,10 @@ package org.koitharu.kotatsu.details.ui
import android.app.ActivityOptions import android.app.ActivityOptions
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import android.widget.AdapterView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.graphics.Insets 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.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment 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.MangaChapter
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.databinding.FragmentChaptersBinding 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.ChaptersAdapter
import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration
import org.koitharu.kotatsu.details.ui.model.ChapterListItem 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 import org.koitharu.kotatsu.reader.ui.ReaderState
class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(), class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
OnListItemClickListener<MangaChapter>, ActionMode.Callback { OnListItemClickListener<MangaChapter>, ActionMode.Callback, AdapterView.OnItemSelectedListener {
private val viewModel by sharedViewModel<DetailsViewModel>() private val viewModel by sharedViewModel<DetailsViewModel>()
@ -53,9 +51,21 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
setHasFixedSize(true) setHasFixedSize(true)
adapter = chaptersAdapter adapter = chaptersAdapter
} }
val branchesAdapter = BranchesAdapter()
binding.spinnerBranches.adapter = branchesAdapter
binding.spinnerBranches.onItemSelectedListener = this
viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged) viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged)
viewModel.chapters.observe(viewLifecycleOwner, this::onChaptersChanged) 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) { viewModel.isChaptersReversed.observe(viewLifecycleOwner) {
activity?.invalidateOptionsMenu() activity?.invalidateOptionsMenu()
} }
@ -64,6 +74,7 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
override fun onDestroyView() { override fun onDestroyView() {
chaptersAdapter = null chaptersAdapter = null
selectionDecoration = null selectionDecoration = null
binding.spinnerBranches.adapter = null
super.onDestroyView() 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 { override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
val manga = viewModel.manga.value val manga = viewModel.manga.value
mode.menuInflater.inflate(R.menu.mode_chapters, menu) mode.menuInflater.inflate(R.menu.mode_chapters, menu)
@ -174,7 +191,7 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
binding.recyclerViewChapters.updatePadding( binding.recyclerViewChapters.updatePadding(
left = insets.left, left = insets.left,
right = insets.right, right = insets.right,
bottom = insets.bottom bottom = insets.bottom + binding.spinnerBranches.height
) )
} }

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.details.ui package org.koitharu.kotatsu.details.ui
import androidx.lifecycle.asFlow
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers 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.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.mapToSet
import java.io.IOException import java.io.IOException
class DetailsViewModel( class DetailsViewModel(
@ -31,6 +33,7 @@ class DetailsViewModel(
) : BaseViewModel() { ) : BaseViewModel() {
private val mangaData = MutableStateFlow<Manga?>(intent.manga) private val mangaData = MutableStateFlow<Manga?>(intent.manga)
private val selectedBranch = MutableStateFlow<String?>(null)
private val history = mangaData.mapNotNull { it?.id } private val history = mangaData.mapNotNull { it?.id }
.distinctUntilChanged() .distinctUntilChanged()
@ -69,12 +72,24 @@ class DetailsViewModel(
val onMangaRemoved = SingleLiveEvent<Manga>() 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( val chapters = combine(
mangaData.map { it?.chapters.orEmpty() }, mangaData.map { it?.chapters.orEmpty() },
history.map { it?.chapterId }, history.map { it?.chapterId },
newChapters, newChapters,
chaptersReversed chaptersReversed,
) { chapters, currentId, newCount, reversed -> selectedBranch
) { chapters, currentId, newCount, reversed, branch ->
val currentIndex = chapters.indexOfFirst { it.id == currentId } val currentIndex = chapters.indexOfFirst { it.id == currentId }
val firstNewIndex = chapters.size - newCount val firstNewIndex = chapters.size - newCount
val res = chapters.mapIndexed { index, chapter -> val res = chapters.mapIndexed { index, chapter ->
@ -86,7 +101,7 @@ class DetailsViewModel(
else -> ChapterExtra.UNREAD else -> ChapterExtra.UNREAD
} }
) )
} }.filter { it.chapter.branch == branch }
if (reversed) res.asReversed() else res if (reversed) res.asReversed() else res
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default) }.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
@ -96,6 +111,15 @@ class DetailsViewModel(
?: throw MangaNotFoundException("Cannot find manga") ?: throw MangaNotFoundException("Cannot find manga")
mangaData.value = manga mangaData.value = manga
manga = manga.source.repository.getDetails(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 mangaData.value = manga
} }
} }
@ -114,4 +138,8 @@ class DetailsViewModel(
fun setChaptersReversed(newValue: Boolean) { fun setChaptersReversed(newValue: Boolean) {
settings.chaptersReverse = newValue 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 var loadingJob: Job? = null
private val currentState = MutableStateFlow<ReaderState?>(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>() private val chapters = LongSparseArray<MangaChapter>()
val readerMode = MutableLiveData<ReaderMode>() val readerMode = MutableLiveData<ReaderMode>()
@ -58,7 +58,7 @@ class ReaderViewModel(
) )
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
val content = MutableLiveData<ReaderContent>(ReaderContent(emptyList(), null)) val content = MutableLiveData(ReaderContent(emptyList(), null))
val manga: Manga? val manga: Manga?
get() = mangaData.value get() = mangaData.value
@ -80,7 +80,6 @@ class ReaderViewModel(
manga.chapters?.forEach { manga.chapters?.forEach {
chapters.put(it.id, it) chapters.put(it.id, it)
} }
mangaData.value = manga
// determine mode // determine mode
val mode = val mode =
dataRepository.getReaderMode(manga.id) ?: manga.chapters?.randomOrNull()?.let { dataRepository.getReaderMode(manga.id) ?: manga.chapters?.randomOrNull()?.let {
@ -96,6 +95,9 @@ class ReaderViewModel(
currentState.value = state ?: historyRepository.getOne(manga)?.let { currentState.value = state ?: historyRepository.getOne(manga)?.let {
ReaderState.from(it) ReaderState.from(it)
} ?: ReaderState.initial(manga) } ?: 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) readerMode.postValue(mode)
val pages = loadChapter(requireNotNull(currentState.value).chapterId) val pages = loadChapter(requireNotNull(currentState.value).chapterId)

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

@ -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" />

@ -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" />

@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.4.30' ext.kotlin_version = '1.4.31'
repositories { repositories {
google() google()
jcenter() jcenter()

Loading…
Cancel
Save